package com.amon.amonplayer.record;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.util.Log;
import android.view.Surface;

import com.amon.amonplayer.opengl.EglHelper;
import com.amon.amonplayer.player.AmonPlayer;
import com.amon.amonplayer.player.AudioInfoListener;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.CyclicBarrier;

import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.opengles.GL;
import javax.microedition.khronos.opengles.GL10;

public abstract class BaseMediaRecorder {
    private final WeakReference<BaseMediaRecorder> mMediaRecorderWr = new WeakReference<>(this);
    private Surface mSurface;
    private EGLContext mEglContext;
    private GLSurfaceView.Renderer mRender;
    private MediaMuxer mMediaMuxer;
    private VideoRenderThread mRenderThread;
    private VideoEncoderThread mVideoEncoderThread;
    private AudioEncoderThread mAudioEncoderThread;
    private MediaCodec mVideoCodec;
    private MediaCodec mAudioCodec;
    private final CyclicBarrier mStartBarrier;
    private final CyclicBarrier mStopBarrier;
    private final AmonPlayer mAmonPlayer;

    public BaseMediaRecorder(Context context, EGLContext eglContext) {
        mEglContext = eglContext;
        mStartBarrier = new CyclicBarrier(2);
        mStopBarrier = new CyclicBarrier(2);
        mAmonPlayer = new AmonPlayer(context);
        mAmonPlayer.setOnPreparedListener(this::start);
        mAmonPlayer.setOnAudioInfoListener(mAudioInfoListener);
    }

    public void setRender(GLSurfaceView.Renderer render) {
        this.mRender = render;
        mRenderThread = new VideoRenderThread(mMediaRecorderWr);
    }

    public void initVideo(String audioPath, String outPath, int width, int height) {
        try {
            mRenderThread.setSize(width, height);
            mMediaMuxer = new MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            intiVideoCodec(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
            mAmonPlayer.setDataSource(audioPath);
            mAmonPlayer.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void initVideo(AssetFileDescriptor audioPath, String outPath, int width, int height) {
        try {
            mRenderThread.setSize(width, height);
            mMediaMuxer = new MediaMuxer(outPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            intiVideoCodec(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
            mAmonPlayer.setDataSource(audioPath.getFileDescriptor());
            mAmonPlayer.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private int getSupportColorFormat(String MIME_TYPE) {
        MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
        MediaCodecInfo[] mediaCodecInfos = mediaCodecList.getCodecInfos();
        MediaCodecInfo codecInfo = null;
        for (MediaCodecInfo info : mediaCodecInfos) {
            if (!info.isEncoder()) {
                continue;
            }
            String[] types = info.getSupportedTypes();
            boolean found = false;
            for (int j = 0; j < types.length && !found; j++) {
                if (types[j].equals(MIME_TYPE)) {
                    Log.e("AvcEncoder", "found support:" + MIME_TYPE);
                    found = true;
                }
            }
            if (!found)
                continue;
            codecInfo = info;
        }
        if (codecInfo == null) {
            return 0;
        }
        MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(MIME_TYPE);
        Log.e("AvcEncoder", "length-" + capabilities.colorFormats.length + "=="
                + Arrays.toString(capabilities.colorFormats));
        int i;
        for (i = 0; i < capabilities.colorFormats.length; i++) {
            switch (capabilities.colorFormats[i]) {
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible:
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface:
                    Log.e("AvcEncoder", "supported color format::" + capabilities.colorFormats[i]);
                    break;
                default:
                    Log.e("AvcEncoder", "other color format " + capabilities.colorFormats[i]);
                    break;
            }
        }
        return 0;//capabilities.colorFormats[i];
    }

    private void intiVideoCodec(String MIME_TYPE, int videoWidth, int videoHeight) throws Exception {
        getSupportColorFormat(MIME_TYPE);

        videoWidth = (videoWidth % 2 == 0) ? videoWidth : videoWidth - 1;
        videoHeight = (videoHeight % 2 == 0) ? videoHeight : videoHeight - 1;

        MediaFormat videoFormat = MediaFormat.createVideoFormat(MIME_TYPE, videoWidth, videoHeight);
        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 6693560);
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        if (Build.VERSION.SDK_INT >= 21) {
            int profile = 0;
            int level = 0;
            if (MIME_TYPE.equals(MediaFormat.MIMETYPE_VIDEO_AVC)) {
                profile = MediaCodecInfo.CodecProfileLevel.AVCProfileHigh;
                if (videoWidth * videoHeight >= 1920 * 1080) {
                    level = MediaCodecInfo.CodecProfileLevel.AVCLevel4;
                } else {
                    level = MediaCodecInfo.CodecProfileLevel.AVCLevel31;
                }
            } else if (MIME_TYPE.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
                profile = MediaCodecInfo.CodecProfileLevel.HEVCProfileMain;
                if (videoWidth * videoHeight >= 1920 * 1080) {
                    level = MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel4;
                } else {
                    level = MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31;
                }
            }
            videoFormat.setInteger(MediaFormat.KEY_PROFILE, profile);
            // API 23以后可以设置AVC的编码level，低于23设置了但不生效
            if (Build.VERSION.SDK_INT >= 23) {
                videoFormat.setInteger(MediaFormat.KEY_LEVEL, level);
            }
        }

        mVideoCodec = MediaCodec.createEncoderByType(MIME_TYPE);
        mVideoCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mSurface = mVideoCodec.createInputSurface();
        mVideoEncoderThread = new VideoEncoderThread(mMediaRecorderWr);
    }

    private void initAudioCodec(int samplesRate, int channel) throws IOException {
        String MIME_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;
        MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, samplesRate, channel);
        audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
        audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, samplesRate * channel * 2);
        mAudioCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        mAudioCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mAudioEncoderThread = new AudioEncoderThread(mMediaRecorderWr);
    }

    protected void start() {
        mAmonPlayer.play();
        mVideoEncoderThread.start();
        mAudioEncoderThread.start();
        mRenderThread.start();
    }

    public void startRecord() {
        mAmonPlayer.prepareAsync();
    }

    public void stopRecord() {
        mAmonPlayer.stop();
        mRenderThread.requestExit();
        mVideoEncoderThread.requestExit();
        mAudioEncoderThread.requestExit();
    }

    private RecordListener mRecordListener;

    public void setOnRecordListener(RecordListener listener) {
        mRecordListener = listener;
    }

    public interface RecordListener {
        void onTime(long times);
    }

    private static final class VideoRenderThread extends Thread {
        private WeakReference<BaseMediaRecorder> mVideoRecorderWr;
        private EglHelper mEglHelper;

        private volatile boolean mShouldExit = false;
        private boolean mHasCreateContext = false;
        private boolean mHasCreateSurface = false;
        private boolean mHasSurfaceChange = false;

        private int mWidth;
        private int mHeight;

        public VideoRenderThread(WeakReference<BaseMediaRecorder> videoRecorderWr) {
            mVideoRecorderWr = videoRecorderWr;
            mEglHelper = new EglHelper();
        }

        @Override
        public void run() {
            try {
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaRecorder videoRecorder = mVideoRecorderWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    if (!mHasCreateContext) {
                        mEglHelper.initCreateEgl(videoRecorder.mSurface, videoRecorder.mEglContext);
                        mHasCreateContext = true;
                    }

                    GL egl10 = mEglHelper.getEglContext().getGL();
                    if (!mHasCreateSurface) {
                        videoRecorder.mRender.onSurfaceCreated((GL10) egl10, mEglHelper.getEGLConfig());
                        mHasCreateSurface = true;
                    }
                    if (!mHasSurfaceChange) {
                        videoRecorder.mRender.onSurfaceChanged((GL10) egl10, mWidth, mHeight);
                        mHasSurfaceChange = true;
                    }
                    videoRecorder.mRender.onDrawFrame((GL10) egl10);
                    mEglHelper.swapBuffers();
                    Thread.sleep(16);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            mEglHelper.destroy();
        }

        private void requestExit() {
            mShouldExit = true;
        }

        public void setSize(int width, int height) {
            this.mWidth = width;
            this.mHeight = height;
        }
    }

    private static final class VideoEncoderThread extends Thread {
        private WeakReference<BaseMediaRecorder> mMediaRecorderWr;
        private final MediaCodec mVideoCodec;
        private final MediaMuxer mMediaMuxer;
        private final MediaCodec.BufferInfo mBufferInfo;
        private final CyclicBarrier mStartBarrier;
        private final CyclicBarrier mStopBarrier;
        private int mVideoTrackIndex = -1;
        private long mPts = 0;

        private volatile boolean mShouldExit = false;

        public VideoEncoderThread(WeakReference<BaseMediaRecorder> mediaRecorderWr) {
            mMediaRecorderWr = mediaRecorderWr;
            mVideoCodec = mMediaRecorderWr.get().mVideoCodec;
            mMediaMuxer = mMediaRecorderWr.get().mMediaMuxer;
            mStartBarrier = mMediaRecorderWr.get().mStartBarrier;
            mStopBarrier = mMediaRecorderWr.get().mStopBarrier;
            mBufferInfo = new MediaCodec.BufferInfo();
        }

        @Override
        public void run() {
            try {
                mVideoCodec.start();
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaRecorder videoRecorder = mMediaRecorderWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    int outputIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                    if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        mVideoTrackIndex = mMediaMuxer.addTrack(mVideoCodec.getOutputFormat());
                        mMediaMuxer.start();
                        mStartBarrier.await();
                    } else {
                        while (outputIndex >= 0) {
                            ByteBuffer outBuffer = mVideoCodec.getOutputBuffer(outputIndex);
                            outBuffer.position(mBufferInfo.offset);
                            outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                            if (mPts == 0) {
                                mPts = mBufferInfo.presentationTimeUs;
                            }
                            mBufferInfo.presentationTimeUs -= mPts;
                            mMediaMuxer.writeSampleData(mVideoTrackIndex, outBuffer, mBufferInfo);
                            if (videoRecorder.mRecordListener != null) {
                                videoRecorder.mRecordListener.onTime(mBufferInfo.presentationTimeUs / 1000);
                            }
                            mVideoCodec.releaseOutputBuffer(outputIndex, false);
                            outputIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            try {
                mVideoCodec.stop();
                mVideoCodec.release();
                mStopBarrier.await();
                mMediaMuxer.stop();
                mMediaMuxer.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void requestExit() {
            mShouldExit = true;
        }
    }

    private final AudioInfoListener mAudioInfoListener = new AudioInfoListener() {
        private long mPts = 0;
        private int mSampleRate;
        private int mChannels;

        @Override
        public void musicInfo(int sampleRate, int channels) {
            try {
                Log.i("TAG", "sampleRate:" + sampleRate + " channels:" + channels);
                mSampleRate = sampleRate;
                mChannels = channels;
                initAudioCodec(sampleRate, channels);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void callbackPcm(byte[] pcmData, int size) {
            int inputBufferIndex = mAudioCodec.dequeueInputBuffer(0);
            if (inputBufferIndex >= 0) {
                ByteBuffer byteBuffer = mAudioCodec.getInputBuffer(inputBufferIndex);
                byteBuffer.clear();
                byteBuffer.put(pcmData);
                mPts += (size * (1.0f * 1000000 / (mSampleRate * mChannels * 2)));
                mAudioCodec.queueInputBuffer(inputBufferIndex,
                        0, size, mPts, 0);
            }
        }
    };

    private static final class AudioEncoderThread extends Thread {
        private final WeakReference<BaseMediaRecorder> mMediaRecorderWr;
        private final MediaCodec mAudioCodec;
        private final MediaMuxer mMediaMuxer;
        private final CyclicBarrier mStartBarrier;
        private final CyclicBarrier mStopBarrier;
        private final MediaCodec.BufferInfo mBufferInfo;
        private long mPts;

        private int mAudioTrackIndex = -1;

        private volatile boolean mShouldExit = false;

        public AudioEncoderThread(WeakReference<BaseMediaRecorder> mediaRecorderWr) {
            mMediaRecorderWr = mediaRecorderWr;
            mAudioCodec = mMediaRecorderWr.get().mAudioCodec;
            mMediaMuxer = mMediaRecorderWr.get().mMediaMuxer;
            mStartBarrier = mMediaRecorderWr.get().mStartBarrier;
            mStopBarrier = mMediaRecorderWr.get().mStopBarrier;
            mBufferInfo = new MediaCodec.BufferInfo();
        }

        @Override
        public void run() {
            try {
                mAudioCodec.start();
                while (true) {
                    if (mShouldExit) {
                        return;
                    }
                    BaseMediaRecorder videoRecorder = mMediaRecorderWr.get();
                    if (videoRecorder == null) {
                        return;
                    }
                    int outputIndex = mAudioCodec.dequeueOutputBuffer(mBufferInfo, 0);
                    if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        mAudioTrackIndex = mMediaMuxer.addTrack(mAudioCodec.getOutputFormat());
                        mStartBarrier.await();
                    } else {
                        while (outputIndex >= 0) {
                            ByteBuffer outBuffer = mAudioCodec.getOutputBuffer(outputIndex);
                            outBuffer.position(mBufferInfo.offset);
                            outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                            if (mPts == 0) {
                                mPts = mBufferInfo.presentationTimeUs;
                            }
                            mBufferInfo.presentationTimeUs -= mPts;
                            mMediaMuxer.writeSampleData(mAudioTrackIndex, outBuffer, mBufferInfo);
                            mAudioCodec.releaseOutputBuffer(outputIndex, false);
                            outputIndex = mAudioCodec.dequeueOutputBuffer(mBufferInfo, 0);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                onDestroy();
            }
        }

        private void onDestroy() {
            try {
                mAudioCodec.stop();
                mAudioCodec.release();
                mStopBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        private void requestExit() {
            mShouldExit = true;
        }
    }
}
